<?php
namespace DRY\Module;

/**
 * DonReY Framework 2012 :: SQL Query Building system.
 *
 * Allows construction of complex SQL statements programatically. Always returns a valid query string, or throws exceptions if there is a problem.
 * Has several dialects for different query languages, those dialects are in "queryfactory" subfolder, named "qf.{dialect}.php"
 *
 * @author Wavetrex <wavetrex@gmail.com>
 * @link http://www.donrey.net/Documentation/Structure/Modules/queryfactory.html
 * @license [GPL v3] http://www.gnu.org/licenses/gpl-3.0.html
 * @version 1.0.dev
 * @package DonReY
 * @subpackage Module.Queryfactory
 */
class Queryfactory extends base
{
	const
		Autoload_Directory = 'queryfactory',
		Autoload_Prefix = 'DRY\Queryfactory\Dialect',
		Autoload_Suffix = 'dialect'
		;

	/**
	 * Creates an Query Factory Statement object, which then has a fluent-code style of assambling the query.
	 * If the created object is used as a string, it will return the assambled statement
	 *
	 * @param string $primary_keyword The word that defines the type of statement ( like CREATE, SELECT, INSERT ... etc. )
	 * @param string $dialect Dialect override
	 */
	public function factory_Statement($primary_keyword, $config=null, $dialect=null)
	{
		$dialect = ($dialect?:(string)$this-> CFG-> dialect)?:'MySQL'; // Default dialect to use: "MySQL" because it's traditionally PHP's partner
		$qf_class = self::Autoload_Prefix.'_'.$dialect;

		// "connection" dependency = Connection module is used by some functions or some engines, in which resulting data depends on the connection modules.
		// Note that for some engines, escaping doesn't work without an active connection ( including MySQL for example )
		return new $qf_class($primary_keyword, $config, $this['connection']);
	}

	/**
	 * Alternative to factory_Statement: $this->select($config,'mysql') instead of $this-> factory_Statement('select',$config,'mysql')
	 *
	 * @param string $primary_keyword (function name)
	 * @param array $args (function arguments - QF config, dialect override)
	 */
	public function __call($primary_keyword,$args)
	{
		return $this ->factory_Statement($primary_keyword,isset($args[0])?$args[0]:null,isset($args[1])?$args[1]:null);
	}

	/**
	 * Converts a string into an array, to be used by the Query Factory, by the following rules:
	 * - "id" -> array( id )
	 * - "id, id, ..." -> array ( id, id, ... )
	 * - "id:alias" -> array ( alias=>id )
	 * - "id:alias, id:alias, ..." -> array ( alias=>id, alias=>id, ... )
	 *
	 * @param string $str Original comma separated values
	 * @return array
	 */
	public static function idents_Str2Array($str)
	{
		$ids_arr = explode(',', $str);
		$ids = array( );
		foreach ($ids_arr as $id_entry) {
			if (strpos($id_entry, ':')!== false) {
				list( $id, $alias ) = explode(':', trim($id_entry));
				$ids [$alias] = $id;
			} else
				$ids [] = trim($id_entry);
		}
		return $ids;
	}

	/**
	 * Converts or creates aliases for use with field list in table() call,
	 * by adding a prefix to the existing alias or creating them from field names
	 * - a string key is simply prefixed {prefix}{old_key}
	 * - a numeric index is converted into {prefix}(string){value}
	 * Since this is only used for arrays containing field names, ALL values must be strings !
	 *
	 * @param string|array $field_list Original field list
	 * @param string $prefix It is added before the field name
	 * @return array Array ready to be passed to the ->table() method as the field list parameter
	 */
	public static function &autoalias_Add_Prefix($fields, $prefix='')
	{
		if($fields===false)
			$fields = array();
		elseif (is_string($fields))
			$fields = self::idents_Str2Array($fields);
		elseif (!is_array($fields))
			return null; // error in field list, not string, not array...

		$keyed=array();
		foreach($fields as $key=>$field_name) {
			if(is_numeric($key))
				$keyed[$prefix.$field_name] = $field_name;
			elseif(is_string($field_name))
				$keyed[$prefix.$key] = $field_name;
			// else error, field ignored
		}
		return $keyed;
	}

}

namespace DRY\Queryfactory;

/**
 * DonReY Framework 2012 :: Query Factory dialects base class (abstract)
 *
 * @author Wavetrex <wavetrex@gmail.com>
 * @link http://www.donrey.net/Documentation/Structure/Modules/queryfactory.html
 * @license [GPL v3] http://www.gnu.org/licenses/gpl-3.0.html
 * @version 1.0.dev
 * @package DonReY
 * @subpackage Module.Queryfactory
 */
abstract class Dialect_base
{
	/**
	 * Primary Statement Keyword. This is the first thing appearing in a statement
	 * @var string
	 */
	protected $psk;

	/**
	 * Configuration passed with the primary keyword (optional)
	 * @var mixed
	 */
	protected $config;

	public function __construct($primary_keyword, $config=null) // various engines might use the config variable
	{
		$this-> psk = ucfirst(strtolower($primary_keyword));
		$this-> config = $config;
	}

	/**
	 * Query that resulted from the execution of compile()
	 *
	 * @var string
	 */
	protected $query;

	/**
	 * Resets the query factory (however keeping primary keyword)
	 *
	 * @param array $what_to_reset (Optional) Indicates which part of the statement to reset (instead of everything)
	 */
	public function reset($what_to_reset=null) {

		$this->query='';
		if(is_array($what_to_reset)) {
			foreach($what_to_reset as $what_to)
				$this->{'qf_'.$what_to} = null;
		} else {
			// iterate the object and resets everything that starts with qf_
			foreach($this as $name=>$value)
				if(substr($name,0,3)=='qf_') {
					$value=null;
					$this->$name=$value;
				}
		}
		$this->query=null;
		return $this;
	}

	/**
	 * Allows to grab QF data using getter functions "get_{what 2 get?}" ( ex: get_Values() )
	 *
	 * @param string $fname
	 * @param array $args
	 * @return mixed return from the SQL engine
	 */
	public function __call($fname,$args) {
		if(substr($fname,0,4)=='get_') {
			$get_what='qf_'.strtolower(substr($fname,4));
			if(isset($this->$get_what))
				return $this->$get_what;
		}
		// when not implemented, just return itself
		return $this;
	}

	/**
	 * General compile - Calls for existing compile parts (implemented in the engine)
	 *
	 * @return DRY_QueryFactory_dialect
	 */
	public function &compile($force = false)
	{
		if(!$force && strlen($this-> query)>0)
			return $this; // do not attempt to recompile if a query string already exists. User force or reset() the query to recompile with the same QF object

		$this-> query = strtoupper($this-> psk).PHP_EOL;

		// runs the compile function specific to the primary keyword (might be captured in a subclass by __call)
		// if that specific compile does not exist, an exception is thrown

		if(!(method_exists($this,'compile_'.$this-> psk)||$this-> accepts_Compile_Method($this-> psk)))
			throw new \Exception("Unable to compile {$this-> psk} statement");
		$this ->{'compile_'.$this ->psk}();

		return $this;
	}

	/**
	 * When QF object is used as a string, the query is auto-compiled and returned.
	 * If compile() encounters an error ( for example incomplete query ) it will throw an exception
	 *
	 * @return string
	 */
	public function &__toString()
	{
		try {
			$this->compile();
		} catch (\Exception $e) {
			// note, since this is a hack, notify core (somehow) that an actual exception has been thrown,
			//   and this is just a way to handle php's problem with exceptions in __toString
			\DRY\water::caught_Exception($e, 'queryfactory', 0);
			$empty = '';
			return $empty;
		}
		return $this->query;
	}

	/**
	 * Override this function when capturing calls to specific statement compilers.
	 * Normally QF throws an exception when method that compiles statement does not exist and this method returns false
	 *
	 * @param string $name
	 */
	public function accepts_Compile_Method($name)
	{
		return false;
	}
}